package com.flame.workflow.ru.engine;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.flame.auth.core.entity.TrustedPrincipal;
import com.flame.core.id.IdWorker;
import com.flame.core.utils.DateUtils;
import com.flame.workflow.constants.NodeConstants;
import com.flame.workflow.constants.RuntimeConstants;
import com.flame.workflow.constants.TaskConstants;
import com.flame.workflow.re.entity.Model;
import com.flame.workflow.re.entity.Node;
import com.flame.workflow.re.mapper.ModelMapper;
import com.flame.workflow.re.service.NodeService;
import com.flame.workflow.ru.engine.context.RuntimeContext;
import com.flame.workflow.ru.engine.event.ErrorFinishInstanceEvent;
import com.flame.workflow.ru.engine.event.ErrorFinishTaskEvent;
import com.flame.workflow.ru.engine.event.FinishInstanceEvent;
import com.flame.workflow.ru.engine.event.FinishTaskEvent;
import com.flame.workflow.ru.engine.event.RunInstanceEvent;
import com.flame.workflow.ru.engine.event.RunTaskEvent;
import com.flame.workflow.ru.engine.event.StartInstanceEvent;
import com.flame.workflow.ru.engine.event.StartTaskEvent;
import com.flame.workflow.ru.engine.handler.TaskHandler;
import com.flame.workflow.ru.entity.Instance;
import com.flame.workflow.ru.entity.Task;
import com.flame.workflow.ru.mapper.InstanceMapper;
import com.flame.workflow.ru.mapper.TaskMapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

/**
 * createInstance     StartInstanceEvent -> RunInstanceEvent
 * runInstance        RunInstanceEvent -> StartTaskEvent/RunTaskEvent
 * createTask      StartTaskEvent -> RunTaskEvent
 * runTask         RunTaskEvent -> FinishTaskEvent
 * finishTask      FinishTaskEvent -> RunInstanceEvent
 * finishInstance     FinishInstanceEvent -> FinishInstanceEvent
 * instanceService    StartInstanceEvent/RunInstanceEvent
 * taskService        RunTaskEvent
 *
 * @author wuxintong😺😺😺
 * @since 2023/6/7
 */
@Service("workflow-runtime-engine")
public class RuntimeEngine implements ApplicationContextAware {

    private final Map<String, TaskHandler> taskHandlerMap = new HashMap<>();

    @Autowired
    private ApplicationEventPublisher publisher;

    @Autowired
    private ModelMapper modelMapper;

    @Autowired
    private IdWorker idWorker;

    @Autowired
    private InstanceMapper instanceMapper;

    @Autowired
    private NodeService nodeService;

    @Autowired
    private TaskMapper taskMapper;

    @Async
    @EventListener
    public void startInstance(StartInstanceEvent event) {
        RuntimeContext runtimeContext = (RuntimeContext) event.getSource();
        Instance instance = runtimeContext.getInstance();
        TrustedPrincipal trustedPrincipal = runtimeContext.getTrustedPrincipal();

        Model dbModel = modelMapper.findById(instance.getModelId(), instance.getModelVersion());
        instance.setInstanceId(idWorker.nextId());
        instance.setInstanceName(dbModel.getModelName());
        instance.setInstanceStatus(RuntimeConstants.RUNTIME_STATUS_CREATE);
        instance.setUserId(trustedPrincipal.getAccount());
        instance.setCreateTime(DateUtils.currentDateTime());

        instanceMapper.insert(instance);
        publisher.publishEvent(new RunInstanceEvent(runtimeContext));
    }

    @Async
    @EventListener
    public void runInstance(RunInstanceEvent event) {
        RuntimeContext runtimeContext = (RuntimeContext) event.getSource();
        Instance instance = runtimeContext.getInstance();

        if (RuntimeConstants.RUNTIME_STATUS_CREATE.equals(instance.getInstanceStatus())) {
            instance.setInstanceStatus(RuntimeConstants.RUNTIME_STATUS_RUN);
            instanceMapper.updateById(instance);
            Task task = new Task();
            task.setNodeId(0);
            runtimeContext.setTask(task);
        }
        publisher.publishEvent(new StartTaskEvent(runtimeContext));
    }

    @Async
    @EventListener
    public void finishInstance(FinishInstanceEvent event) {
        RuntimeContext runtimeContext = (RuntimeContext) event.getSource();
        Instance instance = runtimeContext.getInstance();
        instance.setInstanceStatus(RuntimeConstants.RUNTIME_STATUS_COMPLETE);
        instance.setEndTime(DateUtils.currentDateTime());
        instanceMapper.updateById(instance);
    }

    @Async
    @EventListener
    public void startTask(StartTaskEvent event) {
        RuntimeContext runtimeContext = (RuntimeContext) event.getSource();
        Instance instance = runtimeContext.getInstance();
        Task parentTask = runtimeContext.getTask();

        List<Node> nodeList =
            nodeService.listByParentId(instance.getModelId(), instance.getModelVersion(), parentTask.getNodeId());

        if (CollectionUtils.isEmpty(nodeList) && parentTask.getBranchTaskId() != null) {
            Task branchTask = taskMapper.findById(parentTask.getBranchTaskId());
            Node branchNode =
                nodeService.getNode(instance.getModelId(), instance.getModelVersion(), branchTask.getNodeId());

            RuntimeContext newContext = new RuntimeContext();
            newContext.setInstance(instance);
            newContext.setNode(branchNode);
            newContext.setTask(branchTask);
            publisher.publishEvent(new RunTaskEvent(newContext));
            return;
        }

        boolean createBranchFlag = false;
        if (TaskConstants.TASK_TYPE_CONDITION.equals(parentTask.getTaskType()) ||
            TaskConstants.TASK_TYPE_PARALLEL.equals(parentTask.getTaskType())) {
            createBranchFlag = true;
        }

        for (Node node : nodeList) {
            if (createBranchFlag) {
                if (!NodeConstants.NODE_TYPE_CONDITION_BRANCH.equals(node.getNodeType()) &&
                    !NodeConstants.NODE_TYPE_PARALLEL_BRANCH.equals(node.getNodeType())) {
                    continue;
                }
            }
            RuntimeContext newContext = new RuntimeContext();
            newContext.setInstance(instance);
            newContext.setNode(node);
            newContext.setTask(parentTask);
            newContext.setTrustedPrincipal(runtimeContext.getTrustedPrincipal());
            Task task = taskHandlerMap.get(node.getNodeType()).startTask(newContext);
            newContext.setTask(task);

            publisher.publishEvent(new RunTaskEvent(newContext));
        }
    }

    @Async
    @EventListener
    public void runTask(RunTaskEvent event) {
        RuntimeContext runtimeContext = (RuntimeContext) event.getSource();
        Instance instance = runtimeContext.getInstance();
        Task currentTask = runtimeContext.getTask();

        boolean createFlag = RuntimeConstants.RUNTIME_STATUS_CREATE.equals(currentTask.getTaskStatus());

        if (createFlag) {
            currentTask.setTaskStatus(RuntimeConstants.RUNTIME_STATUS_RUN);
            taskMapper.updateById(currentTask);
        }
        String taskStatus = taskHandlerMap.get(currentTask.getTaskType()).runTask(runtimeContext);
        if (RuntimeConstants.RUNTIME_STATUS_COMPLETE.equals(taskStatus)) {
            publisher.publishEvent(new FinishTaskEvent(runtimeContext));
        } else if (RuntimeConstants.RUNTIME_STATUS_ERROR.equals(taskStatus)) {
            publisher.publishEvent(new ErrorFinishTaskEvent(runtimeContext));
        } else {
            if (createFlag && RuntimeConstants.RUNTIME_STATUS_WAIT.equals(taskStatus)) {
                if (TaskConstants.TASK_TYPE_CONDITION.equals(currentTask.getTaskType()) ||
                    TaskConstants.TASK_TYPE_PARALLEL.equals(currentTask.getTaskType()) ||
                    TaskConstants.TASK_TYPE_CONDITION_BRANCH.equals(currentTask.getTaskType()) ||
                    TaskConstants.TASK_TYPE_PARALLEL_BRANCH.equals(currentTask.getTaskType())) {
                    publisher.publishEvent(new StartTaskEvent(runtimeContext));
                }
            }
            currentTask.setTaskStatus(taskStatus);
            taskMapper.updateById(currentTask);
        }
    }

    @Async
    @EventListener
    public void finishTask(FinishTaskEvent event) {
        synchronized (this) {
            RuntimeContext runtimeContext = (RuntimeContext) event.getSource();
            Instance instance = runtimeContext.getInstance();
            Task currentTask = runtimeContext.getTask();
            currentTask.setTaskStatus(RuntimeConstants.RUNTIME_STATUS_COMPLETE);
            currentTask.setEndTime(DateUtils.currentDateTime());
            taskMapper.updateById(currentTask);

            taskHandlerMap.get(currentTask.getTaskType()).finishTask(runtimeContext);

            if (TaskConstants.TASK_TYPE_CONDITION.equals(currentTask.getTaskType()) ||
                TaskConstants.TASK_TYPE_PARALLEL.equals(currentTask.getTaskType())) {
                boolean flag = true;
                List<Node> nodeList = nodeService
                    .listByParentId(instance.getModelId(), instance.getModelVersion(), currentTask.getNodeId());
                for (Node node : nodeList) {
                    if (!NodeConstants.NODE_TYPE_CONDITION_BRANCH.equals(node.getNodeType()) &&
                        !NodeConstants.NODE_TYPE_PARALLEL_BRANCH.equals(node.getNodeType())) {
                        flag = false;

                        Task dbTask = taskMapper.findByNodeId(instance.getInstanceId(), node.getNodeId());
                        if (dbTask != null) {
                            continue;
                        }

                        RuntimeContext newContext = new RuntimeContext();
                        newContext.setInstance(instance);
                        newContext.setNode(node);
                        newContext.setTask(currentTask);
                        Task task = taskHandlerMap.get(node.getNodeType()).startTask(newContext);
                        newContext.setTask(task);

                        publisher.publishEvent(new RunTaskEvent(newContext));
                    }
                }
                if (flag) {
                    if (null != currentTask.getBranchTaskId()) {
                        Task branchTask = taskMapper.findById(currentTask.getBranchTaskId());
                        Node branchNode = nodeService
                            .getNode(instance.getModelId(), instance.getModelVersion(), branchTask.getNodeId());

                        RuntimeContext newContext = new RuntimeContext();
                        newContext.setInstance(instance);
                        newContext.setTask(branchTask);
                        newContext.setNode(branchNode);
                        publisher.publishEvent(new RunTaskEvent(newContext));
                    }
                }
                return;
            }

            if (TaskConstants.TASK_TYPE_CONDITION_BRANCH.equals(currentTask.getTaskType()) ||
                TaskConstants.TASK_TYPE_PARALLEL_BRANCH.equals(currentTask.getTaskType())) {
                Task branchTask = taskMapper.findById(currentTask.getBranchTaskId());
                Node branchNode =
                    nodeService.getNode(instance.getModelId(), instance.getModelVersion(), branchTask.getNodeId());

                RuntimeContext newContext = new RuntimeContext();
                newContext.setInstance(instance);
                newContext.setNode(branchNode);
                newContext.setTask(branchTask);
                publisher.publishEvent(new RunTaskEvent(newContext));
                return;
            }

            if (TaskConstants.TASK_TYPE_END.equals(currentTask.getTaskType())) {
                publisher.publishEvent(new FinishInstanceEvent(runtimeContext));
                return;
            }

            publisher.publishEvent(new RunInstanceEvent(runtimeContext));
        }
    }

    @Async
    @EventListener
    public void errorFinishTask(ErrorFinishTaskEvent event) {
        synchronized (this) {
            RuntimeContext runtimeContext = (RuntimeContext) event.getSource();
            Task currentTask = runtimeContext.getTask();
            currentTask.setTaskStatus(RuntimeConstants.RUNTIME_STATUS_ERROR);
            currentTask.setEndTime(DateUtils.currentDateTime());
            taskMapper.updateById(currentTask);

            boolean flag = taskHandlerMap.get(currentTask.getTaskType()).handleError(runtimeContext);

            if (flag) {
                publisher.publishEvent(new ErrorFinishInstanceEvent(runtimeContext));
            }
        }
    }

    @Async
    @EventListener
    public void errorFinishInstance(ErrorFinishInstanceEvent event) {
        synchronized (this) {
            RuntimeContext runtimeContext = (RuntimeContext) event.getSource();
            Instance instance = runtimeContext.getInstance();
            instance.setInstanceStatus(RuntimeConstants.RUNTIME_STATUS_ERROR_COMPLETE);
            instance.setEndTime(DateUtils.currentDateTime());
            instanceMapper.updateById(instance);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, TaskHandler> handlerMap = applicationContext.getBeansOfType(TaskHandler.class);
        for (TaskHandler taskHandler : handlerMap.values()) {
            taskHandlerMap.put(taskHandler.taskType(), taskHandler);
        }
    }
}
